Skip to content

S08-06 Node-Koa2

[TOC]

基础

介绍 Koa

前面我们已经学习了 express,另外一个非常流行的 Node Web 服务器框架就是 Koa。

Koa:是由 Express 原团队开发的轻量级 Node.js Web 框架,核心优势是通过 async/await 解决回调地狱,并以 “洋葱模型” 的中间件机制实现灵活的请求 / 响应处理。

定位:node.js 的下一代 web 框架

核心特性

Koa 本身仅包含最基础的 Web 功能(如 HTTP 服务器创建、中间件容器),所有扩展功能(路由、静态文件、请求解析等)均通过第三方中间件实现,设计理念是 “小而精”。

  1. 原生支持 async/await

    彻底摆脱回调函数嵌套问题,中间件可直接用 async 函数编写,错误捕获更简单(通过 try/catch 或错误中间件)。

  2. 洋葱模型中间件

    中间件执行顺序为 “请求进入时从外到内,响应返回时从内到外”,可实现请求预处理(如日志、权限校验)和响应后处理(如统一返回格式)。

    例:中间件 A → 中间件 B → 业务逻辑 → 中间件 B → 中间件 A。

  3. Context 对象封装

    整合 Node 原生的 req(请求对象)和 res(响应对象),提供更简洁的 API,如 ctx.body(设置响应内容)、ctx.query(获取 URL 查询参数)、ctx.params(获取路由参数)。

事实上,koa 是 express 同一个团队开发的一个新的 Web 框架:

  • 目前团队的核心开发者 TJ 的主要精力也在维护 Koa,express 已经交给团队维护了;
  • Koa 旨在为 Web 应用程序和 API 提供更小、更丰富和更强大的能力;
  • 相对于 express 具有更强的异步处理能力(后续我们再对比);
  • Koa 的核心代码只有 1600+行,是一个更加轻量级的框架,我们可以根据需要安装和使用中间件;

安装

依赖安装:

  • koa

    sh
    npm i koa

基本使用

创建 app.js 文件,实现一个基础 HTTP 服务:

js
const Koa = require('koa')

// 1、创建app对象
const app = new Koa()

// 4、注册中间件
app.use((ctx, next) => {
  console.log('匹配到中间件1')
  // 5、返回数据
  ctx.body = '中间件1'
})

// 2、启动服务器
app.listen(8000, () => {
  console.log('koa is running~')
})

// 3、运行服务器

运行命令 node app.js,访问 http://localhost:8000 即可看到响应。

中间件

基本语法

中间件(Middleware):在 Koa 中是处理请求 / 响应的核心机制,所有业务逻辑(如路由、日志、权限校验、错误处理等)都通过中间件实现。Koa 的中间件设计采用 “洋葱模型”,配合 async/await 语法,既能解决回调地狱问题,又能灵活实现请求的 “预处理” 和 “后处理”。Koa 和 Express 的中间件具有相同的功能。

语法

Koa 中间件的本质是一个异步函数,格式如下:

js
async function middleware(ctx, next) {
  // 1. 请求进入时的逻辑(预处理)
  await next(); // 2. 调用 next() 进入下一个中间件
  // 3. 响应返回时的逻辑(后处理)
}

参数

  • ctx:上下文对象,包括请求对象req和响应对象res,见ctx
  • next:本质上是一个返回 Promise 的 dispatch() 函数,类似 Express 的 next(),调用 next() 会暂停当前中间件,移交控制权给下一个中间件;当后续中间件执行完毕后,会回到当前中间件,继续执行 next() 之后的代码。

注意: koa 原生注册中间件只能通过 use() 方法,没有提供 methods 的方式,也没有提供 path 中间件来匹配路径。

js
app.use((ctx, next) => {
  console.log('middleware')
  ctx.response.body = 'Hello World'
})

ctx 参数

ctx(Context,上下文对象)可读写,它整合了 Node 原生的 req(请求对象)和 res(响应对象),并封装了一系列简化 API,让开发者能更便捷地操作请求信息、设置响应内容、处理 cookies 等。

ctx 代理机制

Koa 对 ctx 的属性做了 “代理”,即:

  • 访问 ctx.xxx 时,若 xxx 是请求相关属性(如 urlmethod),则等价于 ctx.request.xxx
  • xxx 是响应相关属性(如 bodystatus),则等价于 ctx.response.xxx
js
ctx.url === ctx.request.url; // true(请求相关)
ctx.body === ctx.response.body; // true(响应相关)
ctx.status === ctx.response.status; // true(响应相关)

例外情况

部分属性仅存在于 requestresponse 中,需直接访问原对象:

  • 请求体 body:需通过 ctx.request.body 获取(ctx.body 是响应体,两者不同)。
  • 原始 Node 对象 req/res:需通过 ctx.req/ctx.res 访问,而非 ctx.request.req
本质与结构

ctx 是 Koa 对请求 / 响应生命周期的 “一站式封装”,其内部包含四个关键对象

  • ctx.reqNode 原生,提供底层请求信息。
  • ctx.resNode 原生,用于底层响应操作。
  • ctx.request:,Koa 封装,新增了 request.queryrequest.body等。
  • ctx.responseKoa 封装,新增了 response.bodyresponse.status 等。
核心属性

请求(ctx.request

  • ctx.request?.method:获取 HTTP 请求方法(大写,如 GETPOST)。
  • ctx.request?.url:获取完整请求 URL(含路径和查询参数,如 /user?name=tom)。
  • ctx.request?.path:获取请求路径(不含查询参数,如 /user)。
  • ctx.request?.query:获取 URL 查询参数(解析为对象,如 ?name=tom&age=18{ name: 'tom', age: '18' })。
  • ctx.request?.querystring:获取原始查询参数字符串(如 name=tom&age=18)。
  • ctx.request?.params:获取路由动态参数(需配合 @koa/router,如 /user/:id{ id: '123' })。
  • ctx.request?.headers:获取请求头信息(对象形式,键名小写,如 { 'user-agent': 'xxx', 'content-type': 'xxx' }
  • ctx.request?.header:与 ctx.headers 完全一致(别名)。
  • ctx.request.body:获取请求体数据(需配合 koa-bodyparser 中间件,支持 JSON / 表单)。
  • ctx.request?.ip:获取客户端 IP 地址。
  • ctx.request?.get()(headerName),获取指定请求头(自动忽略大小写,如 ctx.get('Content-Type'))。

响应(ctx.response

  • ctx.response?.body:设置响应体内容(Koa 会自动根据内容类型设置 Content-Type 和编码)。
  • ctx.response?.status: 设置 HTTP 响应状态码(默认 200),Koa 会自动补充对应状态文本(如 404 → 'Not Found')。
  • ctx.response?.type:设置响应体的 MIME 类型(如 text/htmlapplication/json)。
  • ctx.response?.set()(header, value),设置响应头信息(可传对象批量设置)。
  • ctx.response?.redirect()(url, status?),重定向到指定 URL(默认 302 状态码,可指定第二个参数修改状态码,如 ctx.redirect('/login', 301))。

错误处理方法

  • ctx.throw()(status, msg),抛出错误(自动设置 ctx.status 和错误信息,会被错误中间件捕获)。
  • ctx.assert()(condition, status, msg),断言检查,若 condition 为 false 则抛出错误(类似 throw 的语法糖)。

Cookies 操作

  • ctx.cookies.get()(name, options?),读取 cookies 中指定键的值。
  • ctx.cookies.set()(name, value, options?),设置 cookies(options 支持 maxAgehttpOnlysigned 等)

其他属性方法

  • ctx.app:指向当前 Koa 应用实例(app),可用于访问全局配置或服务。
  • ctx.state:用于中间件间传递数据(推荐的 “共享变量” 容器,避免污染 ctx 本身)。
  • ctx.is()(type),判断请求体类型是否为指定 MIME 类型(如 jsonhtmlform)。
扩展 ctx

在实际开发中,可通过 app.context 扩展 ctx,为所有请求的 ctx 添加全局属性或方法(如数据库实例、工具函数等)。

示例:扩展 ctx 以支持数据库操作

  1. 扩展 ctx.db:添加 db 属性(数据库实例)

    js
    // 扩展 ctx:添加 db 属性(数据库实例)
    app.context.db = {
      queryUser: async (id) => {
        // 模拟数据库查询
        return { id, name: 'tom' };
      }
    };
  2. 使用 ctx.db:在中间件中使用扩展的 ctx.db

    js
    // 在中间件中使用扩展的 ctx.db
    app.use(async (ctx) => {
      const user = await ctx.db.queryUser(1);
      ctx.body = user; // { id: 1, name: 'tom' }
    });

next 参数

next:是串联中间件执行、实现 “洋葱模型” 的核心机制。它的本质是一个返回 Promise 的 dispatch 函数,作用是将当前中间件的执行权移交到下一个中间件,并在后续中间件全部执行完毕后,回到当前中间件继续执行剩余逻辑。

执行机制

核心功能:串联中间件与洋葱模型

  1. 当调用 await next() 时,当前中间件会暂停执行,并将执行权交给下一个注册的中间件
  2. 当后续所有中间件执行完毕(直到某个中间件不调用 next()),执行权会回溯,回到当前中间件,继续执行 next() 之后的代码。

示例:3 个中间件的执行流程

js
// 中间件 1
app.use(async (ctx, next) => {
  console.log('1. 中间件 1 开始(next 前)'); // 1
  await next(); // 移交到中间件 2 // 2
  console.log('1. 中间件 1 结束(next 后)'); // 8
});

// 中间件 2
app.use(async (ctx, next) => {
  console.log('2. 中间件 2 开始(next 前)'); // 3
  await next(); // 移交到中间件 3 // 4
  console.log('2. 中间件 2 结束(next 后)'); // 7
});

// 中间件 3(最内层,不调用 next())
app.use(async (ctx) => {
  console.log('3. 中间件 3 执行(无 next)'); //5
  ctx.body = 'Hello next!'; //6
});

访问服务后,控制台输出顺序为:

plaintext
1. 中间件 1 开始(next 前)
2. 中间件 2 开始(next 前)
3. 中间件 3 执行(无 next)
2. 中间件 2 结束(next 后)
1. 中间件 1 结束(next 后)
执行注意事项

next 的执行机制细节

  1. next() 的调用与中间件执行顺序

    • 中间件按 app.use()注册顺序排列,next() 始终指向 “下一个注册的中间件”;

    • 若某个中间件不调用 next(),则后续中间件不会执行(执行链在此中断)。

      js
      app.use(async (ctx, next) => {
        console.log('中间件 A:不调用 next()');
        // 未调用 next(),中间件 B 不会执行
      });
      
      app.use(async (ctx) => {
        console.log('中间件 B:永远不会执行');
      });
  2. await 对 next() 的必要性

    next() 返回的是 Promise(因为中间件是 async 函数),必须用 await 等待其执行完毕。若省略 await,会导致当前中间件的 “后续逻辑”(next() 之后的代码)提前执行,破坏洋葱模型。

    js
    app.use(async (ctx, next) => {
      console.log('A 开始'); // 1
      next(); // 错误:未用 await
      console.log('A 结束'); // 2
    });
    
    app.use(async (ctx) => {
      console.log('B 执行'); // 3
    });
    
    // 执行顺序:A 开始 -> A 结束 -> B 执行
  3. next() 的返回值

    next() 的返回值是最后一个执行的中间件的返回值(若中间件有返回值)。可利用此特性在中间件间传递数据。

    javascript
    app.use(async (ctx, next) => {
      console.log('外层中间件:开始'); // step1
      // 等待下一个中间件执行,并接收其返回值
      const result = await next(); 
      console.log('外层中间件:收到内层返回值', result); // 输出 "内层数据"  // step3
    });
    
    app.use(async (ctx) => {
      console.log('内层中间件:执行');  // step2
      return '内层数据'; // 返回值会被外层中间件的 next() 接收
    });
    
    // 执行顺序:外层中间件:开始 -- 内层中间件:执行 -- 外层中间件:收到内层返回值 内层数据

路由

koa 官方并没有给我们提供路由的库,我们可以选择第三方库:@koa/router

匹配 path、method

真实开发中我们如何将路径和 method 分离呢?

方式一:根据 request 自己实现

js
app.use((ctx, next) => {
  if (ctx.request.path === '/users') {
    if (ctx.request.method === 'POST') {
      ctx.response.body = 'Create User Success~'
    } else {
      ctx.response.body = 'Users List~'
    }
  } else {
    ctx.response.body = 'Other Request Response'
  }
})

整个代码的逻辑是非常复杂和混乱的,真实开发中我们会使用路由。

方式二:使用第三方路由中间件 @koa/router

@koa/router

安装

依赖安装:

  • @koa/router:koa 官方维护的路由。

    sh
    npm i @koa/router
  • koa-router久不维护,功能和使用与 @koa/router 基本一样。

基本使用

js
const Koa = require('koa')
const Router = require('@koa/router')

const app = new Koa()

// 1. 创建路由对象
const userRouter = new Router({ prefix: '/users' })

// 2. 注册路由中间件
userRouter.get('/', (ctx, next) => {
  ctx.body = 'user列表'
})
userRouter.get('/:id', (ctx, next) => {
  const id = ctx.params.id
  ctx.body = '获取某个用户的信息:' + id
})
userRouter.post('/', (ctx, next) => {
  ctx.body = '创建用户成功~'
})
userRouter.delete('/:id', (ctx, next) => {
  const id = ctx.params.id
  ctx.body = '删除用户成功~' + id
})
userRouter.patch('/:id', (ctx, next) => {
  const id = ctx.params.id
  ctx.body = '修改用户成功~' + id
})

// 3. 挂载路由
app.use(userRouter.routes())

app.listen(8000, () => {
  console.log('koa is running~')
})

allowedMethods()

allowedMethods() 用于判断某一个 method 是否支持:

  • 如果我们请求 get,那么是正常的请求,因为我们有实现 get;
  • 如果我们请求 put、delete、patch,那么就自动报错:Method Not Allowed,状态码:405;
  • 如果我们请求 link、copy、lock,那么就自动报错:Not Implemented,状态码:501;
js
app.use(router.routes());
app.use(router.allowedMethods()); // 放在 routes() 之后

router 前缀

通常一个路由对象是对一组相似路径的封装,那么路径的前缀都是一直的,所以我们可以直接在创建 Router 时,添加前缀

js
+ const userRouter = new Router({ prefix: '/users' })

userRouter.get('/', (ctx, next) => { // 匹配/users/
  ctx.response.body = 'user list~'
})

userRouter.get('/:id', (ctx, next) => { // 匹配/users/:id
  const id = ctx.params.id
  ctx.body = '获取某个用户的信息:' + id
})

module.exports = userRouter

嵌套路由【

路由命名与反向解析【

路由模块化

1、创建userRouter.js文件

js
const KoaRouter = require('@koa/router')

// 1. 创建路由对象
const userRouter = new KoaRouter({ prefix: '/users' })

// 2. 注册路由中间件
userRouter.get('/', (ctx, next) => {
  ctx.body = 'user列表'
})
userRouter.get('/:id', (ctx, next) => {
  const id = ctx.params.id
  ctx.body = '获取某个用户的信息:' + id
})
userRouter.post('/', (ctx, next) => {
  ctx.body = '创建用户成功~'
})
userRouter.delete('/:id', (ctx, next) => {
  const id = ctx.params.id
  ctx.body = '删除用户成功~' + id
})
userRouter.patch('/:id', (ctx, next) => {
  const id = ctx.params.id
  ctx.body = '修改用户成功~' + id
})

// 3. 导出userRouter
module.exports = userRouter

2、导入路由模块并挂载

js
const Koa = require('koa')
// 4. 导入userRouter
const userRouter = require('./d03-userRouter')

const app = new Koa()

// 5. 挂载路由
app.use(userRouter.routes())
app.use(userRouter.allowedMethods())

app.listen(8000, () => {
  console.log('koa is running~')
})

API

js
const router = new Router({ prefix: '/xxx' })
new Router()

说明: 创建一个路由器(Router)实例

语法

js
const router = new Router(options?)

参数

  • options?:``,
    • prefix?:``,为路由器中所有路由定义的 URL 前缀。默认为 ""

返回值

  • router:``,路由实例
router.routes()

说明: 将路由器中定义的路由添加到 Koa 应用程序中

语法

js
router.routes()

参数: void

返回值: 返回一个路由中间件

示例

js
// 将路由器中定义的路由添加到Koa应用程序中
app.use(router.routes())
router.METHODS()

说明: 包括一系列的请求方法:

  • router.get():``
  • router.post():``
  • router.delete():``
  • router.patch():``
  • router.put():``
  • router.head():``
  • router.options():``

语法

js
router.get(path, middleware)

参数

  • pathstring | reg,要匹配的 URL 路径模式
  • middleware:``,中间件函数,可以添加多个中间件

返回值: undefined

示例

js
// 定义一个简单的 GET 请求路由
router.get('/hello', (ctx, next) => {
  ctx.body = 'Hello, Koa!'
})
router.allowedMethods()

说明: 一个中间件函数,用于处理在路由处理之后、但未发送响应之前的阶段。它的作用是根据请求的方法(GET、POST、PUT 等)来检查路由是否允许该方法,并进行适当的处理。

未在后端封装的方法,前端请求时会返回Method Not Allowed,而不是Not Found

语法

js
router.allowedMethods(options?)

参数

  • options?:``,
    • throw:``,当请求方法不匹配时,是否抛出错误。默认为 true
    • notImplemented:``,未实现的 HTTP 方法的响应状态码。默认为 501
    • methodNotAllowed:``,不允许的 HTTP 方法的响应状态码。默认为 405

返回值: 返回一个路由中间件

示例

js
// 定义路由
router.get('/users', (ctx, next) => {
  // 处理 GET /users 请求
})

router.post('/users', (ctx, next) => {
  // 处理 POST /users 请求
})

// 加载路由中间件
app.use(router.routes())
;+app.use(router.allowedMethods())

请求

请求参数解析

客户端传递到服务器参数的方法常见的是 5 种:

  • 方式一:通过 get 请求中的 URL 的 params;
  • 方式二:通过 get 请求中的 URL 的 query;
  • 方式三:通过 post 请求中的 body 的 json 格式;
  • 方式四:通过 post 请求中的 body 的 x-www-form-urlencoded 格式;
  • 方式五:通过 post 请求中的 form-data 格式;

GET 发送 query

请求地址/login?username=why&password=123

获取参数:通过 ctx.queryctx.request.query 获取参数。

注意: 通过ctx.query获取的参数是对象格式(同 express 一致)。

js
// 1. query:?name=tom&age=18
userRouter.get('/', (ctx, next) => {
  console.log(ctx.query) // { name: 'tom', age: '18' }
  console.log(ctx.request.query) // { name: 'tom', age: '18' }
  console.log(ctx.query === ctx.request.query) // true

  ctx.body = JSON.stringify(ctx.query)
})

GET 发送 params

请求地址/users/123

获取参数:通过 ctx.params 配合动态路由 /:id 获取参数 123。

js
// 2. params
userRouter.get('/:id', (ctx, next) => {
  console.log(ctx.params.id) // 123
  ctx.body = 'Hello World'
})

POST 发送 JSON

依赖安装:


请求地址/login

请求参数: body 是 json 格式:

js
{
    "username": "coderwhy",
    "password": "123"
}

解析参数

  • 1、挂载中间件 app.use(bodyParser())

  • 2、通过 ctx.rquest.body 获取 json 参数。

  • 注意: 不能从 ctx.bodyctx.req.body 中获取数据。

js
// 1. 挂载中间件,解析 json 参数
app.use(bodyParser())

app.use((ctx, next) => {
  // 2. 通过 ctx.request.body 获取 json 参数
  console.log(ctx.request.body)
    
  ctx.body = 'Hello World'
})

POST 发送 x-www-form-urlencoded

依赖安装:


请求地址/login

请求参数: body 是 x-www-form-urlencoded 格式

image-20251025115207423

获取参数和 json 一致

  • 1、挂载中间件 app.use(bodyParser())

  • 2、通过 ctx.rquest.body 获取 json 参数

  • 注意: 不能从 ctx.bodyctx.req.body 中获取数据。

js
// 1. 挂载中间件,解析 json 参数
app.use(bodyParser())

app.use((ctx, next) => {
  // 2. 通过 ctx.request.body 获取 json 参数
  console.log(ctx.request.body)
    
  ctx.body = 'Hello World'
})

POST 发送 form-data

依赖安装:


请求地址/login

请求参数: body 是 form-data 格式

image-20251025115807845

获取参数

  • 1、挂载中间件 @koa/multer
  • 2、通过 ctx.request.body 获取 form-data 参数
js
// 1. 引入 @koa/multer
const multer = require('@koa/multer')

// 2. 创建 upload 对象
const upload = multer({})

// 3. 对 form-data 请求单独使用 upload.any() 返回的中间件,解析参数
router.post('/formData', upload.any(), (ctx, next) => {
    
  // 4. 通过 ctx.request.body 获取 form-data 参数
  console.log(ctx.request.body)
    
  ctx.body = ctx.request.body
})

响应

ctx.body

响应方式:

  • ctx.body :Koa 中通过 ctx.body 响应数据

响应数据类型

  • string :字符串数据
  • Buffer :Buffer 数据
  • Stream :流数据
  • Object|| Array:对象或者数组
  • null :不输出任何内容

示例

js
// 1. string
ctx.response.body = 'Hello World'

// 2. object
ctx.body = {
  name: 'why',
  age: 18,
  height: 1.88
}

// 3. array
ctx.body = ['tom', 'jack', 'mark']

// 4. stream
const readableStreamTxt = fs.createReadStream('./data/stream.txt')
const readableStreamImg = fs.createReadStream('./data/plane.webp')
console.log(readableStreamImg)
ctx.type = 'image/webp'
ctx.body = readableStreamImg

// 5. buffer
const buf = Buffer.from('你好,Koa')
ctx.body = buf

// 6. null
ctx.body = null // status 为 204 No Content

对比 ctx.response.body

js
ctx.body === ctx.response.body
ctx.body !== ctx.request.body
  • 事实上,我们访问 ctx.body 时,本质上是访问 ctx.response.body
  • 我们可以看到源码中,我们访问 proto(这里就是 ctx),其实是访问 proto 中的 response 的属性

image-20240206124038126

ctx.status

设置方式:

即可以通过 ctx.status 设置,也可以通过 ctx.response.status 设置

  • ctx.status
  • ctx.response.status

注意: 如果 ctx.status 尚未设置,Koa 会自动将状态设置为 200204

文件上传

单文件上传

基本使用

依赖安装:


思路

  • 1、通过 multer({dest})dest 选项设置上传文件的目标目录
  • 2、通过在 /single 路由中调用 upload.single('plane') 中间件,实现单文件上传
  • 3、单文件的信息可以通过 ctx.filectx.request.file 查看

注意: 通过 dest 的方法无法自定义文件名,并且上传的文件没有后缀名

js
const Koa = require('koa')
const KoaRouter = require('@koa/router')
const multer = require('@koa/multer')

const app = new Koa()
const uploadRouter = new KoaRouter({ prefix: '/upload' })

// 文件上传-单文件
// 1. 创建 upload 对象
const upload = multer({ dest: './uploads' })

// 2. 在路由中使用 upload.single() 返回的中间件解析上传的文件
uploadRouter.post('/single', upload.single('plane'), (ctx, next) => {
    
  // 3. 通过 ctx.file 或 ctx.request.file 获取上传的文件信息
  console.log(ctx.file)
  if (ctx.file.filename) {
    ctx.body = '文件上传成功~'
  }
})

app.use(uploadRouter.routes())

app.listen(8000, () => {
  console.log('koa is running~')
})

优化:自定义文件名

思路: 通过 multer({storage: multer.diskStorage(destination, filename)}) 的方式设置上传文件的目标目录,可以自定义文件名,并且上传的文件也有后缀名

js
// 文件上传-单文件
// 自定义文件名和存储目录
const upload = multer({
  storage: multer.diskStorage({
    destination(req, file, callback) {
      callback(null, './upload')
    },
    filename(req, file, callback) {
      const newName =
        file.originalname.replace(path.extname(file.originalname), '') +
        '_' +
        Date.now() +
        path.extname(file.originalname)
      callback(null, newName)
    }
  })
})

uploadRouter.post('/single', upload.single('plane'), (ctx, next) => {
  console.log(ctx.file)
  if (ctx.file.filename) {
    ctx.body = '文件上传成功~'
  }
})

多文件上传

思路

  • 1、通过 multer({storage: multer.diskStorage(destination, filename)}) 的方式设置上传文件的目标目录(和单文件一样
  • 2、通过在 /multi 路由中调用 upload.array('vision') 中间件,实现多文件上传
  • 3、多文件的信息可以通过 ctx.files 数组查看
js
const path = require('path')
const Koa = require('koa')
const KoaRouter = require('@koa/router')
const multer = require('@koa/multer')

const app = new Koa()
const uploadRouter = new KoaRouter({ prefix: '/upload' })

// 文件上传-多文件
const upload = multer({
  storage: multer.diskStorage({
    destination(req, file, callback) {
      callback(null, './upload')
    },
    filename(req, file, callback) {
      const newName =
        file.originalname.replace(path.extname(file.originalname), '') +
        '_' +
        Date.now() +
        path.extname(file.originalname)
      console.log(newName)
      callback(null, newName)
    }
  })
})

// 1. 区别于单文件:在路由中使用 upload.array() 返回的中间件
uploadRouter.post('/multi', upload.array('vision'), (ctx, next) => {
    
  // 2. 区别于单文件:通过 ctx.files 获取上传的文件列表
  console.log(ctx.files)
  ctx.body = '文件上传成功~'
})

app.use(uploadRouter.routes())

app.listen(8000, () => {
  console.log('koa is running~')
})

静态资源服务器

基本使用

依赖安装


部署静态资源服务器:部署的过程类似于 express

js
const Koa = require('koa')
const static = require('koa-static') // 注意:不是 @koa/static

const app = new Koa()

app.use(static('./build'))

app.listen(8000, () => {
  console.log('静态服务器启动成功~')
})

错误处理

错误处理

1、发射错误:在需要抛出错误的位置通过 ctx.app.emit() 发射 error 事件。

  • ctx.app :可以获取当前应用程序的实例。
  • app.emit(eventName, ...args):触发事件并传递参数给监听器回调函数。
js
userRouter.get('/', (ctx, next) => {
  const isError = true
  if (isError) {
    // 1. 发射错误事件
    ctx.app.emit('error', -1004, ctx)
  } else {
    ctx.body = '用户列表~'
  }
})

2、统一处理错误:在统一的位置处理抛出的错误。

  • app.on(eventName, listener):注册一个事件监听器,当事件被触发时执行回调函数。
js
// 2. 监听事件,集中处理错误信息
app.on('error', (code, ctx) => {
  let message = ''
  switch (code) {
    case -1001:
      message = '请求地址错误'
      break
    case -1002:
      message = '资源错误'
      break
    default:
      message = '其他错误'
  }
  ctx.body = { code, message }
})

对比 express

架构设计

  • express:完整和强大的,内置了很多好用的功能
  • koa:简洁和自由的,只包含最核心功能(甚至没有最基本的 get/post 请求),不限制使用其他中间件

执行同步

express 和 koa 框架的核心都是中间件,但是他们的执行机制不同,特别是中间件中包含异步操作时。

需求:假如有三个中间件会在一次请求中匹配到,并且按照顺序执行;

  • 在 middleware1 中,在 req.message 中添加一个字符串 aaa
  • 在 middleware2 中,在 req.message 中添加一个 字符串bbb
  • 在 middleware3 中,在 req.message 中添加一个 字符串ccc
  • 当所有内容添加结束后,在 middleware1 中,通过 res 返回最终的结果
  • 注意: 是在 middleware1 中返回 res

对比执行同步

同步执行时 express 和 koa 没有区别

  1. 通过 express 同步实现

    执行顺序:middleware1 -> middleware2 -> middleware3 -> middleware2 -> middleware1

    js
    const express = require('express')
    
    const app = express()
    
    const middleware1 = (req, res, next) => {
      req.message = 'aaa' // 1. 添加 aaa
      next()
      res.end(req.message) // 4. 返回最终结果:aaabbbccc
    }
    
    const middleware2 = (req, res, next) => {
      req.message = req.message + 'bbb' // 2. 添加 bbb
      next()
    }
    
    const middleware3 = (req, res, next) => {
      req.message = req.message + 'ccc' // 3. 添加 ccc
    }
    
    app.use(middleware1, middleware2, middleware3)
    
    app.listen(8000, () => {
      console.log('启动成功~')
    })

    最终的结果是:aaabbbccc,没问题

  2. 通过 koa 同步实现

    执行顺序:middleware1 -> middleware2 -> middleware3 -> middleware2 -> middleware1

    js
    const Koa = require('koa')
    
    const app = new Koa()
    
    const middleware1 = (ctx, next) => {
      ctx.message = 'aaa' // 1. 添加 aaa
      next()
      console.log('aaaa')
      ctx.body = ctx.message // 4. 返回最终结果:aaabbbccc
    }
    
    const middleware2 = (ctx, next) => {
      ctx.message += 'bbb' // 2. 添加 bbb
      console.log('bbbb')
      next()
    }
    
    const middleware3 = (ctx, next) => {
      ctx.message += 'ccc' // 3. 添加 ccc
    }
    
    app.use(middleware1)
    app.use(middleware2)
    app.use(middleware3)
    
    app.listen(8000, () => {
      console.log('启动成功~')
    })

    最终的结果也是:aaabbbccc,也没问题

执行异步

如果我们最后的 ccc 中的结果,是需要异步操作才能获取到的,是否会产生问题呢?

思路

  • koa 中如果希望等待下一个异步函数的执行结果,需要在 next 函数前加上 await
  • express 中添加 await 的方法无效

对比执行异步

异步执行时 koa 会在执行 await next() 后等待后续异步操作的结果,而 express 不会。

原理

  • koa 中的 next() 函数返回的是 Promise,因此可以使用 await。
  • express 中的 next() 函数返回的是 void,因此 await 无效。
  1. express 中遇到异步操作

    1. 没有在 next 前,加 async、await

      js
      const middleware1 = (req, res, next) => {
        req.message = 'aaa' // 1. 添加 aaa
        next()
        res.end(req.message) // 4. 返回最终结果:aaabbb ❌
      }
      
      const middleware2 = (req, res, next) => {
        req.message += 'bbb' // 2. 添加 bbb
        next()
      }
      
      const middleware3 = async (req, res, next) => {
        const result = await axios.get('http://123.207.32.32:9001/lyric?id=167876') // 异步操作
        req.message += result.data.lrc.lyric // 3. 添加 异步请求数据
      }

      最终结果aaabbb,是不正确。

    2. 有在 next 前,加 async、await

      js
      const middleware1 = async (req, res, next) => {
        req.message = 'aaa' // 1. 添加 aaa
        await next()
        res.end(req.message) // 4. 返回最终结果:aaabbb ❌
      }
      
      const middleware2 = async (req, res, next) => {
        req.message += 'bbb' // 2. 添加 bbb
        await next()
      }
      
      const middleware3 = async (req, res, next) => {
        const result = await axios.get('http://123.207.32.32:9001/lyric?id=167876') // 异步操作
        req.message += result.data.lrc.lyric // 3. 添加 异步请求数据
        console.log(req.message)
      }

      最终结果aaabbb,也是不正确。

    3. 原因分析

      因为本质上 next() 和异步没有任何关系,next() 本身是一个同步函数的调用,不会等到异步有结果之后再继续执行后续的操作。

  2. koa 中遇到异步操作

    1. 没有在 next 前,加 async、await

      js
      const middleware1 = async (ctx, next) => {
        ctx.message = 'aaa' // 1. 添加 aaa
        next()
        ctx.body = ctx.message // 4. 返回最终结果:aaabbb ❌
      }
      
      const middleware2 = async (ctx, next) => {
        ctx.message += 'bbb' // 2. 添加 bbb
        next()
      }
      
      const middleware3 = async (ctx, next) => {
        const result = await axios.get('http://123.207.32.32:9001/lyric?id=167876') // 异步操作
        ctx.message += result.data.lrc.lyric // 3. 添加 异步请求数据
      }

      最终结果aaabbb,也是不正确。

      原因分析

      因为虽然 next() 函数是一个返回 Promise 的异步操作,但不加 await 也是不会等待结果的返回的,而是会继续向后执行了。

    2. 有在 next 前,加 async、await

      js
      const middleware1 = async (ctx, next) => {
        ctx.message = 'aaa' // 1. 添加 aaa
        await next()
        ctx.body = ctx.message // 4. 返回最终结果:aaabbb歌词信息 ✅
      }
      
      const middleware2 = async (ctx, next) => {
        ctx.message += 'bbb' // 2. 添加 bbb
        await next()
      }
      
      const middleware3 = async (ctx, next) => {
        const result = await axios.get('http://123.207.32.32:9001/lyric?id=167876') // 异步操作
        ctx.message += result.data.lrc.lyric // 3. 添加 异步请求数据
      }

      最终结果aaabbb+歌词信息,是正确。

      原因分析

      因为当在 koa 中的 next 前面加 await 时,它会等到后续有一个确定结果时,再执行后续的代码。

image-20240206170345870

洋葱模型

洋葱模型(Onion Model)是 Koa 框架中用来描述中间件执行流程的一种模型。这个模型得名于中间件的执行方式,就像剥洋葱一样,请求和响应穿过一系列中间件,每个中间件都有机会在请求到达和离开时执行特定的逻辑。

洋葱模型的执行顺序可以简单描述为:请求从外层中间件开始处理,然后依次向内层传递;在内层中间件处理完毕后又依次向外层传递响应。这种模型可以让开发者清晰地了解中间件的执行顺序,并便于对请求和响应进行各种处理。

注意

  • Koa 中无论同步、异步都符合洋葱模型

  • Express 中同步符合洋葱模型,异步不符合

image-20251025162358456

koa 中异步请求的执行顺序

image-20240206165419516

源码

new Koa()

1、

js
+++ const app = new Koa()

2、new Koa()本质上是new Application()

js
module.exports = class Application extends Emitter {
  constructor(options) {
    super();
    options = options || {};
    this.proxy = options.proxy || false;
    this.subdomainOffset = options.subdomainOffset || 2;
    this.proxyIpHeader = options.proxyIpHeader || 'X-Forwarded-For';
    this.maxIpsCount = options.maxIpsCount || 0;
    this.env = options.env || process.env.NODE_ENV || 'development';
    if (options.keys) this.keys = options.keys;

    // 用于保存中间件fns
++    this.middleware = [];

+    this.context = Object.create(context);
+    this.request = Object.create(request);
+    this.response = Object.create(response);
    // util.inspect.custom support for node 6+
    /* istanbul ignore else */
    if (util.inspect.custom) {
      this[util.inspect.custom] = this.inspect;
    }
    if (options.asyncLocalStorage) {
      const { AsyncLocalStorage } = require('async_hooks');
      assert(AsyncLocalStorage, 'Requires node 12.17.0 or higher to enable asyncLocalStorage');
      this.ctxStorage = new AsyncLocalStorage();
    }
  }
};

app.listen()

js
+++ app.listen(8000, () => {
  console.log('koa is running~')
})
js
  listen(...args) {
    debug('listen');
+++    const server = http.createServer(this.callback());
    return server.listen(...args);
  }
js
module.exports = class Application extends Emitter {
  constructor(options) { // 省略 }
++  callback() {
    const fn = compose(this.middleware);

    if (!this.listenerCount('error')) this.on('error', this.onerror);

+    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);
      if (!this.ctxStorage) {
        return this.handleRequest(ctx, fn);
      }
      return this.ctxStorage.run(ctx, async() => {
        return await this.handleRequest(ctx, fn);
      });
    };

+    return handleRequest;
  }
}

app.use()

1、注册中间件

js
+++ app.use((ctx, next) => {
  console.log('匹配到中间件1')
  // 5、返回数据
  ctx.body = '中间件1'
})
js
module.exports = class Application extends Emitter {
  constructor(options) { // 省略 }
+  use(fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    if (isGeneratorFunction(fn)) {
      deprecate('Support for generators will be removed in v3. ' +
                'See the documentation for examples of how to convert old middleware ' +
                'https://github.com/koajs/koa/blob/master/docs/migration.md');
      fn = convert(fn);
    }
    debug('use %s', fn._name || fn.name || '-');
++    this.middleware.push(fn);
    return this;
  }
}

2、请求的处理过程

js
  listen(...args) {
    debug('listen');
+++    const server = http.createServer(this.callback());
    return server.listen(...args);
  }
js
module.exports = class Application extends Emitter {
  constructor(options) { // 省略 }
+  callback() {
+    const fn = compose(this.middleware);

    if (!this.listenerCount('error')) this.on('error', this.onerror);

    const handleRequest = (req, res) => {
+      const ctx = this.createContext(req, res);
      if (!this.ctxStorage) {
+++        return this.handleRequest(ctx, fn);
      }
      return this.ctxStorage.run(ctx, async() => {
        return await this.handleRequest(ctx, fn);
      });
    };

+    return handleRequest;
  }
}
js
  handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404;
    const onerror = err => ctx.onerror(err);
    const handleResponse = () => respond(ctx);
    onFinished(res, onerror);
+++    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }
js
function compose (middleware) {
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

  /**
   * @param {Object} context
   * @return {Promise}
   * @api public
   */

  return function (context, next) {
    // last called middleware #
    let index = -1
     // dispatch本质上就是next()
+    return dispatch(0)
+    function dispatch (i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
++        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

API

Application

  • app.listen(port, callback):``,启动一个 HTTP 服务器并监听指定端口
  • app.use():``,用来注册中间件的方法
  • app.emit():``,触发事件并传递参数给监听器回调函数。
  • app.on():``,注册一个事件监听器,当事件被触发时执行回调函数。

Context

属性

  • Node 原生对象
  • ctx.req:原生 Node.js 的 request 对象。
  • ctx.res:原生 Node.js 的 response 对象。
  • Koa 封装对象
  • ctx.request:Koa 封装后的请求对象,包含了请求头、请求体等信息。
  • ctx.response:Koa 封装后的响应对象,包含了响应头、响应体等信息。
  • 请求
  • ctx.method:HTTP 请求的方法,如 GET、POST 等。
  • ctx.url:请求的 URL 地址,不包含域名部分。
  • ctx.path:请求的路径,不包含查询参数部分。
  • 获取请求参数
  • ctx.query:请求的 query 参数,以对象形式返回。
  • ctx.params:请求的 params 参数
  • ctx.request.body:请求的 post 参数,包括 json, x-www-form-urlencoded, form-data
  • 响应
  • ctx.body:响应的主体内容。
  • ctx.status:响应的状态码,默认为 404。
  • ctx.type:设置响应的 Content-Type 头部字段
  • ctx.app:获取当前应用程序的实例
  • ctx.cookies:Cookies 对象,用于读取和设置 Cookies。

Request

  • ctx.request.body:``,请求的 post 参数,包括 json, x-www-form-urlencoded, form-data

Response

  • ctx.response.body:``,等价于 ctx.body

中间件

@koa/router

@koa/router

koa-bodyparser

初始化

js
const bodyParser = require('koa-bodyparser')
app.use(bodyParser())
  • bodyParser():挂载 bodyParser

multer

安装pnpm add @koa/multer

请求格式multipart/form-data

应用: 文件上传

初始化

js
const multer = require('@koa/multer')
  • multer(options?):``,处理文件上传。它基于 busboy 构建,可帮助你方便地处理通过表单上传的文件
    • options?:``,
      • dest?:``,指定上传文件的保存路径
      • storage?:``,详细指定上传文件的保存路径和文件名
      • fileFilter?:``,用于过滤上传文件的回调函数
  • multer.diskStorage({destination, filename}),用于配置磁盘存储引擎的函数,可以作为storage选项的值
  • multer.memoryStorage():``,用于配置内存存储引擎的函数
  • upload.single(fieldname),用于处理单个文件上传的中间件。可以通过req.file访问上传的文件
    • fieldname:``,上传文件的字段名,必须与 HTML 表单中 input 标签的 name 属性相同
  • upload.array(fieldname, maxCount?),用于处理多文件上传。可以通过req.files访问上传的文件
    • fieldname:``,上传文件的字段名,必须与 HTML 表单中 input 标签的 name 属性相同
    • maxCount?:``,允许上传的最大文件数量,默认值为 Infinity
    • 注意: 所有上传文件的字段名都必须一样
  • upload.fields(),用于处理文件上传的中间件函数。可以通过req.files访问上传的文件
    • fields:[{name: fieldname, maxCount?},...],用于定义文件上传的配置
    • 注意: 所有上传文件的字段名可以单独设置
  • upload.any(),解析通过form-data格式请求的数据。可以通过req.body获取解析后的数据
    • 注意: 不推荐使用form-data格式请求参数。通常使用form-data格式上传文件

@koa/multer

multer 的包装

koa-static

初始化

js
const serve = require('koa-static')
app.use(serve('./xxx')) // xxx为静态服务器的根目录
  • serve(root):``,返回一个静态服务器的中间件
    • root:``,静态服务器的根目录